/*
 * DLM PDF.js Proxy Viewer
 * Contributor: Ron Fredericks, BiophysicsLab.com
 * Snippet: DLM PDF.js Proxy Viewer
 * Rev: 2
 * Last updated: 3/31/2026
 *
 * Provides a protected PDF proxy and viewer shortcode for files managed
 * by the Download Monitor plugin.
 *
 * Features:
 * - Shortcode [dlm_pdfjs id="123"] renders a link to open the PDF in a
 *   new tab using the PDF.js viewer with full viewer controls
 * - Link text defaults to the Download Monitor title, or can be overridden
 *   with the name= attribute: [dlm_pdfjs id="123" name="My PDF Title"]
 * - Proxy streams protected DLM PDF files securely from uploads
 * - Auto-detects PDF path from Download Monitor metadata
 * - Caches resolved path in post meta for performance
 * - New downloads auto-resolve on first proxy request via 7-step resolver
 * - Allows manual fallback map for stubborn files
 * - Counting is handled by the companion snippet:
 *     DLM PDF.js Proxy Counter
 *
 * Shortcode usage:
 *   [dlm_pdfjs id="123"]
 *   [dlm_pdfjs id="123" name="My PDF Title"]
 *
 * Typical page usage:
 *   [dlm_pdfjs id="123"]
 *   [file_event_count_live id="123"] [file_event_date_live id="123"]
 *
 * IMPORTANT:
 * This snippet calls ron_increment_file_event($download_id, 'proxy') from the
 * companion snippet for unified counting of PDF views.
 * ZIP file counting is handled via JavaScript click interception in the
 * companion snippet - no server-side hook is used for ZIPs, making counting
 * fully cache-independent.
 */

/*
 * OPTIONAL MANUAL FALLBACKS
 *
 * The auto-detect system resolves PDF paths automatically from Download Monitor
 * metadata and caches the result in post meta (_ron_pdfjs_relative_path).
 * In most cases this array should remain empty.
 *
 * How to override auto-detect system:
 *
 * Add an entry here only if auto-detect fails for a specific download ID.
 * What follows is a description of the example presented here.
 *
 * The download_id for each file managed by the Download Monitor plugin can be
 * found in the WordPress Dashboard → Downloads → All Downloads: Shortcode column.
 *
 * Path format: relative to /wp-content/uploads/
 * For Download Monitor files this is typically: dlm_uploads/YYYY/MM/filename.pdf
 *
 * Example:
 *   3873 => 'dlm_uploads/2026/03/The_Transistor_1951_v4.pdf',
 */
function ron_dlm_pdfjs_manual_map() {
    return array(
        // Example:
        // 3873 => 'dlm_uploads/2026/03/The_Transistor_1951_v4.pdf',
    );
}

/*
 * Shortcode: [dlm_pdfjs id="123"]
 * Renders a link that opens the protected PDF in a new tab using PDF.js viewer.
 * Link text defaults to the Download Monitor title for the download.
 * Optional name= attribute overrides the link text.
 */
add_shortcode('dlm_pdfjs', 'ron_dlm_pdfjs_link_shortcode');
function ron_dlm_pdfjs_link_shortcode($atts) {
    $atts = shortcode_atts(
        array(
            'id'   => 0,
            'name' => '',
        ),
        $atts,
        'dlm_pdfjs'
    );

    $download_id = absint($atts['id']);
    if (!$download_id) {
        return '';
    }

    $viewer_file = 'viewer.php';
    $viewer_disk_path = WP_CONTENT_DIR . '/plugins/pdfjs-viewer-shortcode/pdfjs/web/' . $viewer_file;
    if ( ! file_exists( $viewer_disk_path ) ) {
        return '';
    }

    $proxy_url = add_query_arg(
        array('dlm_pdf_proxy' => $download_id),
        home_url('/')
    );

    $viewer_url = home_url('/wp-content/plugins/pdfjs-viewer-shortcode/pdfjs/web/' . $viewer_file)
        . '?file=' . rawurlencode($proxy_url);

    $label = !empty($atts['name'])
        ? esc_html($atts['name'])
        : esc_html(get_the_title($download_id));

    return '<a href="' . esc_url($viewer_url) . '" target="_blank">'
        . $label
        . '</a>';
}

/*
 * Handle the proxy request.
 */
add_action('template_redirect', 'ron_dlm_handle_pdf_proxy', 1);
function ron_dlm_handle_pdf_proxy() {
    if (!isset($_GET['dlm_pdf_proxy'])) {
        return;
    }

    $download_id = absint($_GET['dlm_pdf_proxy']);
    if (!$download_id) {
        ron_dlm_proxy_die(400, 'Missing download ID.');
    }

    if (!function_exists('wp_get_upload_dir')) {
        ron_dlm_proxy_die(500, 'WordPress upload functions unavailable.');
    }

    $download_post = get_post($download_id);
    if (!$download_post) {
        ron_dlm_proxy_die(404, 'Download not found.');
    }

    $relative_path = ron_dlm_pdfjs_get_cached_relative_pdf_path($download_id);
    if (!$relative_path) {
        $manual = ron_dlm_pdfjs_manual_map();
        if (isset($manual[$download_id])) {
            $relative_path = ltrim($manual[$download_id], '/');
        }
    }

    /*
     * If cache and manual map both failed, run the full resolver.
     * This handles new downloads that have never been proxied before
     * and therefore have no cached path yet. The resolver will also
     * write the cache so subsequent requests use the fast path.
     */
    if (!$relative_path) {
        $relative_path = ron_dlm_pdfjs_resolve_relative_pdf_path($download_id, null, $download_post);
    }

    if (!$relative_path) {
        ron_dlm_proxy_die(404, 'No cached PDF path found for this download.');
    }

    $file_path = ron_dlm_pdfjs_relative_to_absolute_path($relative_path);
    if (!$file_path) {
        ron_dlm_proxy_die(404, 'Could not resolve a readable PDF file for this download.');
    }

    $real = realpath($file_path);
    if (!$real || !is_file($real) || !is_readable($real)) {
        ron_dlm_proxy_die(404, 'Protected PDF file not found or not readable.');
    }

    $uploads = wp_get_upload_dir();
    $uploads_real = !empty($uploads['basedir']) ? realpath($uploads['basedir']) : false;

    if (!$uploads_real || strpos($real, $uploads_real) !== 0) {
        ron_dlm_proxy_die(403, 'Resolved file is outside uploads directory.');
    }

    if (strtolower(pathinfo($real, PATHINFO_EXTENSION)) !== 'pdf') {
        ron_dlm_proxy_die(403, 'This proxy only serves PDF files.');
    }

    /*
     * Unified counting:
     * Pass 'proxy' as source so the companion snippet applies the
     * byte-range duplicate lock appropriate for PDF.js requests.
     */
    if (function_exists('ron_increment_file_event')) {
        ron_increment_file_event($download_id, 'proxy');
    }

    nocache_headers();
    header('Content-Type: application/pdf');
    header('Content-Length: ' . filesize($real));
    header('Content-Disposition: inline; filename="' . basename($real) . '"');
    header('Accept-Ranges: bytes');
    header('X-Content-Type-Options: nosniff');

    readfile($real);
    exit;
}

/*
 * Main resolver.
 * Returns uploads-relative path like:
 * dlm_uploads/2026/03/file.pdf
 *
 * 7-step resolution chain:
 * 1) Manual fallback map
 * 2) Cached post meta (_ron_pdfjs_relative_path)
 * 3) Version object getters (get_path, get_url, get_file_url, etc.)
 * 4) Nested file object getters
 * 5) Download object getters + version children object getters
 *    + _files meta on version child post (key lookup for new DLM uploads)
 * 6) Common post meta keys on download post
 * 7) Returns false if all steps fail
 */
function ron_dlm_pdfjs_resolve_relative_pdf_path($download_id, $version = null, $download = null) {
    // 1) manual fallback wins
    $manual = ron_dlm_pdfjs_manual_map();
    if (isset($manual[$download_id])) {
        $relative = ron_dlm_pdfjs_normalize_relative_path($manual[$download_id]);
        if ($relative && ron_dlm_pdfjs_relative_to_absolute_path($relative)) {
            ron_dlm_pdfjs_cache_relative_pdf_path($download_id, $relative);
            return $relative;
        }
    }

    // 2) cached path
    $cached = ron_dlm_pdfjs_get_cached_relative_pdf_path($download_id);
    if ($cached && ron_dlm_pdfjs_relative_to_absolute_path($cached)) {
        return $cached;
    }

    $candidates = array();

    // 3) version object from filter call
    if (is_object($version)) {
        $candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($version));
    }

    // 4) nested file object if available
    if (is_object($version) && method_exists($version, 'get_file')) {
        $file = $version->get_file();
        if (is_object($file)) {
            $candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($file));
        }
    }

    // 5) download object from filter call
    if (is_object($download)) {
        $candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($download));

        if (method_exists($download, 'get_versions')) {
            $versions = $download->get_versions();
            if (is_array($versions)) {
                foreach ($versions as $v) {
                    if (is_object($v)) {
                        $candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($v));
                        if (method_exists($v, 'get_file')) {
                            $file = $v->get_file();
                            if (is_object($file)) {
                                $candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($file));
                            }
                        }
                    }
                }
            }
        }

        /*
         * Also check _files meta on the version child post directly.
         * Download Monitor stores file URLs as a JSON-encoded array in
         * this meta key. This runs once after the version loop, not inside it.
         * This is the key lookup for new downloads added via the DLM UI.
         */
        $version_posts = get_posts(array(
            'post_type'      => 'dlm_download_version',
            'post_parent'    => $download_id,
            'posts_per_page' => 1,
            'post_status'    => 'any',
        ));
        if (!empty($version_posts)) {
            $files_raw = get_post_meta($version_posts[0]->ID, '_files', true);
            if (is_string($files_raw) && $files_raw !== '') {
                $files_decoded = json_decode($files_raw, true);
                if (is_array($files_decoded) && !empty($files_decoded)) {
                    foreach ($files_decoded as $file_url) {
                        if (is_string($file_url) && $file_url !== '') {
                            $candidates[] = $file_url;
                        }
                    }
                } else {
                    $candidates[] = $files_raw;
                }
            }
        }
    }

    // 6) common post meta fallbacks
    $meta_keys = array(
        'dlm_download_version',
        'dlm_version',
        'downloadable_file',
        'file',
    );

    foreach ($meta_keys as $key) {
        $value = get_post_meta($download_id, $key, true);
        if (is_string($value) && $value !== '') {
            $candidates[] = $value;
        }
    }

    foreach ($candidates as $candidate) {
        $relative = ron_dlm_pdfjs_candidate_to_relative_pdf_path($candidate);
        if ($relative) {
            ron_dlm_pdfjs_cache_relative_pdf_path($download_id, $relative);
            return $relative;
        }
    }

    return false;
}

/*
 * Pull likely path/url strings from an object using common getters.
 */
function ron_dlm_pdfjs_extract_candidates_from_object($object) {
    $out = array();

    $methods = array(
        'get_path',
        'get_file_path',
        'get_url',
        'get_file_url',
        'get_filename',
    );

    foreach ($methods as $method) {
        if (method_exists($object, $method)) {
            try {
                $value = $object->$method();
                if (is_string($value) && $value !== '') {
                    $out[] = $value;
                }
            } catch (Throwable $e) {
            }
        }
    }

    return $out;
}

/*
 * Convert any candidate string to uploads-relative PDF path if possible.
 */
function ron_dlm_pdfjs_candidate_to_relative_pdf_path($candidate) {
    if (!is_string($candidate) || $candidate === '') {
        return false;
    }

    $candidate = trim($candidate);

    // URL inside uploads
    if (filter_var($candidate, FILTER_VALIDATE_URL)) {
        $relative = ron_dlm_pdfjs_convert_uploads_url_to_relative($candidate);
        if ($relative && strtolower(pathinfo($relative, PATHINFO_EXTENSION)) === 'pdf') {
            return $relative;
        }
        return false;
    }

    // Absolute path
    if ($candidate[0] === '/' || strpos($candidate, ':') !== false) {
        $relative = ron_dlm_pdfjs_convert_absolute_path_to_relative($candidate);
        if ($relative && strtolower(pathinfo($relative, PATHINFO_EXTENSION)) === 'pdf') {
            return $relative;
        }
        return false;
    }

    // Relative path
    $relative = ron_dlm_pdfjs_normalize_relative_path($candidate);
    if ($relative && strtolower(pathinfo($relative, PATHINFO_EXTENSION)) === 'pdf') {
        $absolute = ron_dlm_pdfjs_relative_to_absolute_path($relative);
        if ($absolute) {
            return $relative;
        }
    }

    return false;
}

/*
 * Convert uploads URL to uploads-relative path.
 */
function ron_dlm_pdfjs_convert_uploads_url_to_relative($url) {
    if (!function_exists('wp_get_upload_dir')) {
        return false;
    }

    $uploads = wp_get_upload_dir();
    if (empty($uploads['baseurl'])) {
        return false;
    }

    if (strpos($url, $uploads['baseurl']) !== 0) {
        return false;
    }

    $relative = substr($url, strlen($uploads['baseurl']));
    return ron_dlm_pdfjs_normalize_relative_path($relative);
}

/*
 * Convert absolute uploads path to uploads-relative path.
 */
function ron_dlm_pdfjs_convert_absolute_path_to_relative($path) {
    if (!function_exists('wp_get_upload_dir')) {
        return false;
    }

    $uploads = wp_get_upload_dir();
    if (empty($uploads['basedir'])) {
        return false;
    }

    $uploads_real = realpath($uploads['basedir']);
    $path_real    = realpath($path);

    if (!$uploads_real || !$path_real) {
        return false;
    }

    if (strpos($path_real, $uploads_real) !== 0) {
        return false;
    }

    $relative = substr($path_real, strlen($uploads_real));
    return ron_dlm_pdfjs_normalize_relative_path($relative);
}

/*
 * Normalize relative path.
 */
function ron_dlm_pdfjs_normalize_relative_path($relative) {
    if (!is_string($relative) || $relative === '') {
        return false;
    }

    $relative = str_replace('\\', '/', $relative);
    $relative = ltrim($relative, '/');

    if ($relative === '') {
        return false;
    }

    return $relative;
}

/*
 * Convert uploads-relative path to absolute path if readable.
 */
function ron_dlm_pdfjs_relative_to_absolute_path($relative) {
    if (!function_exists('wp_get_upload_dir')) {
        return false;
    }

    $uploads = wp_get_upload_dir();
    if (empty($uploads['basedir'])) {
        return false;
    }

    $full = trailingslashit($uploads['basedir']) . ltrim($relative, '/');
    $real = realpath($full);

    if (!$real || !is_file($real) || !is_readable($real)) {
        return false;
    }

    return $real;
}

/*
 * Cache resolved relative path.
 */
function ron_dlm_pdfjs_cache_relative_pdf_path($download_id, $relative_path) {
    update_post_meta($download_id, '_ron_pdfjs_relative_path', $relative_path);
}

/*
 * Get cached relative path.
 */
function ron_dlm_pdfjs_get_cached_relative_pdf_path($download_id) {
    $value = get_post_meta($download_id, '_ron_pdfjs_relative_path', true);
    if (!is_string($value) || $value === '') {
        return false;
    }
    return ron_dlm_pdfjs_normalize_relative_path($value);
}

/*
 * Friendly proxy errors for testing.
 */
function ron_dlm_proxy_die($status, $message) {
    status_header((int) $status);
    nocache_headers();
    wp_die(
        esc_html($message),
        'PDF Proxy',
        array(
            'response' => (int) $status,
        )
    );
}
